/* Copyright (c) 2009 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.opensocial;

import org.opensocial.models.Model;
import org.opensocial.providers.Provider;

import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

/**
 * OpenSocial data request; encapsulates all information required to execute a
 * request including RESTful URL template, RPC method name, and any parameters
 * required by the provider. Typically, Request objects aren't instantiated by
 * clients directly but are instead returned by service class methods. Requests
 * are executed by passing them into a {@link Client}'s send method.
 *
 * @author Jason Cooper
 *
 */
public class Request {

  public static final String GUID = "guid";
  public static final String GROUP_ID = "groupId";
  public static final String SELECTOR = "selector";
  public static final String APP_ID = "appid";
  public static final String P_ID = "pid";

  public static final String ALBUM_ID = "albumId";
  public static final String ITEM_ID = "itemId";

  public static final String MOOD_ID = "moodId";
  public static final String FRIEND_ID = "friendId";
  public static final String HISTORY = "history";


  private String rpcMethod;
  private String restMethod;
  private String restUrlTemplate;
  private String contentType;
  private byte[] customPayload;

  private Map<String, String> components;
  private Map<String, Object> rpcPayloadParameters;
  private Map<String, Object> restPayloadParameters;
  private Map<String, String> rpcQueryStringParameters;
  private Map<String, String> restQueryStringParameters;

  private Class<? extends Model> modelClass;

  /**
   * Creates and returns a new {@link Request} configured with the passed
   * RESTful URL template, RPC method name, and HTTP method.
   *
   * @param restUrlTemplate RESTful URL template to use when executing request
   *                        using REST
   * @param rpcMethod       RPC method name to use when executing request via
   *                        a RPC
   * @param restMethod      HTTP method to use when executing request using
   *                        REST
   */
  public Request(String restUrlTemplate, String rpcMethod, String restMethod) {
    this.rpcMethod = rpcMethod;
    this.restMethod = restMethod;
    this.restUrlTemplate = restUrlTemplate;

    this.components = new HashMap<String, String>();
  }

  /**
   * Returns the associated RPC method name, e.g. people.get.
   */
  public String getRpcMethod() {
    return rpcMethod;
  }

  /**
   * Returns the associated HTTP method for REST requests, e.g. GET or POST.
   */
  public String getRestMethod() {
    return restMethod;
  }

  /**
   * Returns the RESTful URL template for the requested resource type, e.g.
   * people/{guid}/{selector}/{pid}.
   */
  public String getRestUrlTemplate() {
    return restUrlTemplate;
  }

  /**
   * Returns the request's custom content (MIME) type if provided; if null, the
   * {@link Client} will set the Content-Type request header to a default type
   * specified in the {@link Provider} class passed into it, usually
   * application/json.
   */
  public String getContentType() {
    return contentType;
  }

  /**
   * Returns the request's custom payload if provided; if not null, this value
   * overrides the JSON payload generated by the {@link Client}.
   */
  public byte[] getCustomPayload() {
    return customPayload;
  }

  /**
   * Returns the request component with the specified name, e.g. guid or
   * groupId, or null if no request component with the specified name exists.
   *
   * @param name Name of the request component to return.
   */
  public String getComponent(String name) {
    return components.get(name);
  }

  /**
   * Returns the set of name-value pairs used by the {@link Client} to generate
   * the JSON payload for a RPC request; guaranteed not to be null.
   */
  public Map<String, Object> getRpcPayloadParameters() {
    if (rpcPayloadParameters == null) {
      rpcPayloadParameters = new HashMap<String, Object>();
    }

    return rpcPayloadParameters;
  }

  /**
   * Returns the set of name-value pairs used by the {@link Client} to generate
   * the JSON payload for a RESTful request; guaranteed not to be null.
   */
  public Map<String, Object> getRestPayloadParameters() {
    if (restPayloadParameters == null) {
      restPayloadParameters = new HashMap<String, Object>();
    }

    return restPayloadParameters;
  }

  /**
   * Returns the set of name-value pairs used by the {@link Client} to generate
   * the query string that gets appended to the RPC endpoint; guaranteed not
   * to be null.
   */
  public Map<String, String> getRpcQueryStringParameters() {
    if (rpcQueryStringParameters == null) {
      rpcQueryStringParameters = new HashMap<String, String>();
    }

    return rpcQueryStringParameters;
  }

  /**
   * Returns the set of name-value pairs used by the {@link Client} to generate
   * the query string that gets appended to the RESTful URL; guaranteed not to
   * be null.
   */
  public Map<String, String> getRestQueryStringParameters() {
    if (restQueryStringParameters == null) {
      restQueryStringParameters = new HashMap<String, String>();
    }

    return restQueryStringParameters;
  }

  /**
   * Returns the model class that developers use to access response data after
   * a request has been successfully executed.
   */
  public Class<? extends Model> getModelClass() {
    if (modelClass == null) {
      return Model.class;
    }

    return modelClass;
  }

  /**
   * Sets the REST URL template for this request.
   *
   * @param template REST URL template for this request
   */
  public void setRestUrlTemplate(String template) {
    this.restUrlTemplate = template;
  }

  /**
   * Sets the request's guid component which is passed as a parameter when the
   * request is executed.
   *
   * @param guid GUID (OpenSocial user ID) to set or "@me" for the current
   *             viewer
   */
  public void setGuid(String guid) {
    addComponent(GUID, guid);
    addRpcPayloadParameter("userId", guid);
  }

  /**
   * Sets the request's appid component which is passed as a parameter when the
   * request is executed.
   *
   * @param appId application ID to set or "@app" for the current application
   */
  public void setAppId(String appId) {
    addComponent(APP_ID, appId);
    addRpcPayloadParameter("appId", appId);
  }

  /**
   * Sets the request's groupId component which is passed as a parameter when
   * the request is executed.
   *
   * @param groupId OpenSocial group ID to set, usually "@self" or "@friends"
   */
  public void setGroupId(String groupId) {
    addComponent(GROUP_ID, groupId);
    addRpcPayloadParameter("groupId", groupId);
  }

  /**
   * Sets the request's selector component which is passed as a parameter when
   * the request is executed.
   *
   * @param selector OpenSocial selector to set, usually "@self" or "@friends"
   */
  public void setSelector(String selector) {
    addComponent(SELECTOR, selector);
    addRpcPayloadParameter("groupId", selector);
  }

  /**
   * Sets the request's pId (person ID) component which is passed as a
   * parameter when the request is executed.
   *
   * @param pid OpenSocial pId to set
   */
  public void setPId(String pid) {
    addComponent(P_ID, pid);
    addRpcPayloadParameter("pid", pid);
  }

  /**
   * Sets a custom content (MIME) type which is later used as the value of the
   * Content-Type request header when the request is executed; leave null
   * except when passing a custom payload such as binary image data.
   *
   * @param contentType custom Content-Type request header value to set
   */
  public void setContentType(String contentType) {
    this.contentType = contentType;
  }

  /**
   * Sets the payload to pass in the corresponding HTTP request's body as POST
   * data when the request is executed, overriding the JSON payload generated
   * by the {@link Client}; leave null except when uploading special content,
   * e.g. binary image data, and make sure to call setContentType as well.
   *
   * @param payload custom request payload to set
   */
  public void setCustomPayload(byte[] payload) {
    this.customPayload = payload;
  }

  /**
   * Sets the request component with the specified name to the specified value.
   *
   * @param name  name of request component to set
   * @param value new value of specified request component
   */
  public void addComponent(String name, String value) {
    components.put(name, value);
  }

  /**
   * Adds an extended request parameter, e.g. count or startIndex, to be passed
   * as query parameters in the URL if executed using REST or in the request
   * body if executed via a RPC.
   *
   * @param name  name of the extended parameter to set
   * @param value value of the specified extended parameter
   */
  public void addParameter(String name, String value) {
    addRpcPayloadParameter(name, value);
    addRestQueryStringParameter(name, value);
  }

  /**
   * Adds extended request parameters for each name-value pair in the passed
   * {@link Map}.
   *
   * @param parameters Map of name-value pairs to pass as extended request
   *                   parameters
   * @see              #addParameter(String, String)
   */
  public void addParameters(Map<String, String> parameters) {
    for (Map.Entry<String, String> parameterEntry: parameters.entrySet()) {
      addParameter(parameterEntry.getKey(), parameterEntry.getValue());
    }
  }

  /**
   * Sets the "count" extended request parameter to the specified value; used
   * to specify the page size for a paged collection.
   *
   * @param count new value of "count" extended request parameter
   * @see         #addParameter(String, String)
   * @see         #setStartIndexParameter(int)
   */
  public void setCountParameter(int count) {
    addParameter("count", "" + count);
  }

  /**
   * Sets the "startIndex" extended request parameter to the specified value;
   * used to specify the index into a paged collection.
   *
   * @param startIndex new value of "startIndex" extended request parameter
   * @see              #addParameter(String, String)
   * @see              #setCountParameter(int)
   */
  public void setStartIndexParameter(int startIndex) {
    addParameter("startIndex", "" + startIndex);
  }

  /**
   * Sets the "fields" extended request parameter to the specified value; used
   * to specify which field names to include in the representation or in the
   * members of a collection.
   *
   * @param fields array of field names to pass as extended request parameter
   * @see          #addParameter(String, String)
   */
  public void setFieldsParameter(String[] fields) {
    StringBuilder builder = new StringBuilder();

    for (String field : fields) {
      if (builder.length() > 0) {
        builder.append(",");
      }
      builder.append(field);
    }

    addRestQueryStringParameter("fields", builder.toString());
    addRpcPayloadParameter("fields", Arrays.asList(fields));
  }

  /**
   * Adds a new parameter with the specified name and value to the set of
   * parameters used by the {@link Client} to generate the JSON payload for a
   * RPC request; this method should not be called by clients directly unless
   * custom requests are being executed.
   *
   * @param name  name of RPC payload parameter to set
   * @param value value of the specified RPC payload parameter, usually of type
   *              String, List, or Map
   */
  public void addRpcPayloadParameter(String name, Object value) {
    if (rpcPayloadParameters == null) {
      rpcPayloadParameters = new HashMap<String, Object>();
    }

    rpcPayloadParameters.put(name, value);
  }

  /**
   * Adds a new parameter with the specified name and value to the set of
   * parameters used by the {@link Client} to generate the JSON payload for a
   * REST request; this method should not be called by clients directly unless
   * custom requests are being executed.
   *
   * @param name  name of REST payload parameter to set
   * @param value value of the specified REST payload parameter, usually of
   *              type String, List, or Map
   */
  public void addRestPayloadParameter(String name, Object value) {
    if (restPayloadParameters == null) {
      restPayloadParameters = new HashMap<String, Object>();
    }

    restPayloadParameters.put(name, value);
  }

  /**
   * For each property in the passed model object, adds a new parameter to the
   * set of parameters used by the {@link Client} to generate the JSON payload
   * for a REST request ; this method should not be called by clients directly
   * unless custom requests are being executed.
   *
   * @param modelObject Map of name-value pairs to pass as REST payload
   *                    parameters
   */
  public <T extends Model> void addRestPayloadParameters(T modelObject) {
    for (Map.Entry modelEntry : (Set<Map.Entry>) modelObject.entrySet()) {
      addRestPayloadParameter((String) modelEntry.getKey(),
          modelEntry.getValue());
    }
  }

  /**
   * Adds a new parameter with the specified name and value to the set of
   * parameters used by the {@link Client} to generate the query string that
   * gets appended to the RPC endpoint; this method should not be called by
   * clients directly unless custom requests are being executed.
   *
   * @param name  name of the RPC query string parameter to set
   * @param value value of the specified RPC query string parameter
   */
  public void addRpcQueryStringParameter(String name, String value) {
    if (rpcQueryStringParameters == null) {
      rpcQueryStringParameters = new HashMap<String, String>();
    }

    rpcQueryStringParameters.put(name, value);
  }

  /**
   * Adds a new parameter with the specified name and value to the set of
   * parameters used by the {@link Client} to generate the query string that
   * gets appended to the RESTful URL; this method should not be called by
   * clients directly unless custom requests are being executed.
   * 
   * @param name  name of the REST query string parameter to set
   * @param value value of the specified REST query string parameter
   */
  public void addRestQueryStringParameter(String name, String value) {
    if (restQueryStringParameters == null) {
      restQueryStringParameters = new HashMap<String, String>();
    }

    restQueryStringParameters.put(name, value);
  }

  /**
   * Sets the model class that developers will use to access response data
   * after a request has been successfully executed; this method should not be
   * called by clients directly unless custom requests are being executed.
   *
   * @param modelClass class extending {@link Model} to use to access response
   *                   data
   */
  public void setModelClass(Class<? extends Model> modelClass) {
    this.modelClass = modelClass;
  }
}
